<?php
/**
 * 阿里云服务签名V3版本
 */
class AliSignature
{
    // 加密算法
    private $ALGORITHM;
    // Access Key ID
    private $AccessKeyId;
    // Access Key Secret
    private $AccessKeySecret;

    public function __construct($AccessKeyId, $AccessKeySecret)

    {
        date_default_timezone_set('UTC'); // 设置时区为GMT
        $this->AccessKeyId = $AccessKeyId; 
        $this->AccessKeySecret = $AccessKeySecret; 
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 设置加密算法
    }

    /**
     * 签名示例，您需要根据实际情况替换main方法中的示例参数。
     * ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同，其余内容都是相似的。
     *
     * 通过API元数据获取请求方法（methods）、请求参数名称（name）、请求参数类型（type）、请求参数位置（in），并将参数封装到SignatureRequest中。
     * 1. 请求参数在元数据中显示"in":"query"，通过queryParam传参。注：RPC接口该类型参数也支持通过body传参，content-type为application/x-www-form-urlencoded，参见示例三。
     * 2. 请求参数在元数据中显示"in": "body"，通过body传参，MIME类型为application/octet-stream、application/json。RPC接口不建议使用application/json，可使用示例三替代。
     * 3. 请求参数在元数据中显示"in": "formData"，通过body传参，MIME类型为application/x-www-form-urlencoded。
     */
    public function main()
    {
        // RPC接口请求示例一：请求参数"in":"query"
        $request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
        // DescribeInstanceStatus请求参数如下：
        $request['queryParam'] = [
            // RegionId在元数据中显示的类型是String，"in":"query"，必填
            'RegionId' => 'cn-hangzhou',
            // InstanceId的在元数据中显示的类型是array，"in":"query"，非必填
            'InstanceId' => ["i-bp11ht4h2kdXXXXXXXX", "i-bp16maz3h3xgXXXXXXXX", "i-bp10r67hmslXXXXXXXX"]
        ];


        // RPC接口请求示例三：请求参数"in": "formData"或"in":"body"（非上传文件场景）
        $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
        // TranslateGeneral请求参数如下：
        $request['queryParam'] = [
            // Context在元数据中显示的类型是String，"in":"query"，非必填
            'Context' => '早上',
        ];
        $formData = [
            'FormatType' => 'text',
            'SourceLanguage' => 'zh',
            'TargetLanguage' => 'en',
            'SourceText' => '你好',
            'Scene' => 'general',
        ];
        $str = self::formDataToString($formData);
        $request['body'] = $str;
        $request['headers']['content-type'] = 'application/x-www-form-urlencoded';

        $this->getAuthorization($request);
        // 调用API
        $this->callApi($request);
    }

    private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
    {
        $headers = [
            'host' => $host,
            'x-acs-action' => $xAcsAction,
            'x-acs-version' => $xAcsVersion,
            'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
            'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
        ];
        return [
            'httpMethod' => $httpMethod,
            'canonicalUri' => $canonicalUri,
            'host' => $host,
            'headers' => $headers,
            'queryParam' => [],
            'body' => null,
        ];
    }

    private function getAuthorization(&$request)
    {
        $request['queryParam'] = $this->processObject($request['queryParam']);
        $canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
        $hashedRequestPayload = hash('sha256', $request['body'] ?? '');
        $request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;

        $canonicalHeaders = $this->buildCanonicalHeaders($request['headers']);
        $signedHeaders = $this->buildSignedHeaders($request['headers']);

        $canonicalRequest = implode("\n", [
            $request['httpMethod'],
            $request['canonicalUri'],
            $canonicalQueryString,
            $canonicalHeaders,
            $signedHeaders,
            $hashedRequestPayload,
        ]);

        $hashedCanonicalRequest = hash('sha256', $canonicalRequest);
        $stringToSign = "{$this->ALGORITHM}\n$hashedCanonicalRequest";

        $signature = strtolower(bin2hex(hash_hmac('sha256', $stringToSign, $this->AccessKeySecret, true)));
        $authorization = "{$this->ALGORITHM} Credential={$this->AccessKeyId},SignedHeaders=$signedHeaders,Signature=$signature";

        $request['headers']['Authorization'] = $authorization;
    }

    private function callApi($request)
    {
        try {
            // 通过cURL发送请求
            $url = "https://" . $request['host'] . $request['canonicalUri'];

            // 添加请求参数到URL
            if (!empty($request['queryParam'])) {
                $url .= '?' . http_build_query($request['queryParam']);
            }

            echo $url;
            // 初始化cURL会话
            $ch = curl_init();

            // 设置cURL选项
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 禁用SSL证书验证，请注意，这会降低安全性，不应在生产环境中使用（不推荐！！！）
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回而不是输出内容
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // 添加请求头

            // 根据请求类型设置cURL选项
            switch ($request['httpMethod']) {
                case "GET":
                    break;
                case "POST":
                    curl_setopt($ch, CURLOPT_POST, true);
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
                    break;
                case "DELETE":
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
                    break;
                default:
                    echo "Unsupported HTTP method: " . $request['body'];
                    throw new Exception("Unsupported HTTP method");
            }

            // 发送请求
            $result = curl_exec($ch);

            // 检查是否有错误发生
            if (curl_errno($ch)) {
                echo "Failed to send request: " . curl_error($ch);
            } else {
                echo $result;
            }

        } catch (Exception $e) {
            echo "Error: " . $e->getMessage();
        } finally {
            // 关闭cURL会话
            curl_close($ch);
        }
    }

    function formDataToString($formData)
    {
        $res = self::processObject($formData);
        return http_build_query($res);
    }

    function processObject($value)
    {
        // 如果值为空，则无需进一步处理
        if ($value === null) {
            return;
        }
        $tmp = [];
        foreach ($value as $k => $v) {
            if (0 !== strpos($k, '_')) {
                $tmp[$k] = $v;
            }
        }
        return self::flatten($tmp);
    }

    private static function flatten($items = [], $delimiter = '.', $prepend = '')
    {
        $flatten = [];
        foreach ($items as $key => $value) {
            $pos = \is_int($key) ? $key + 1 : $key;

            if (\is_object($value)) {
                $value = get_object_vars($value);
            }

            if (\is_array($value) && !empty($value)) {
                $flatten = array_merge(
                    $flatten,
                    self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
                );
            } else {
                if (\is_bool($value)) {
                    $value = true === $value ? 'true' : 'false';
                }
                $flatten["$prepend$pos"] = $value;
            }
        }
        return $flatten;
    }


    private function convertHeadersToArray($headers)
    {
        $headerArray = [];
        foreach ($headers as $key => $value) {
            $headerArray[] = "$key: $value";
        }
        return $headerArray;
    }


    private function buildCanonicalQueryString($queryParams)
    {

        ksort($queryParams);
        // Build and encode query parameters
        $params = [];
        foreach ($queryParams as $k => $v) {
            if (null === $v) {
                continue;
            }
            $str = rawurlencode($k);
            if ('' !== $v && null !== $v) {
                $str .= '=' . rawurlencode($v);
            } else {
                $str .= '=';
            }
            $params[] = $str;
        }
        return implode('&', $params);
    }

    private function buildCanonicalHeaders($headers)
    {
        // Sort headers by key and concatenate them
        uksort($headers, 'strcasecmp');
        $canonicalHeaders = '';
        foreach ($headers as $key => $value) {
            $canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
        }
        return $canonicalHeaders;
    }

    private function buildSignedHeaders($headers)
    {
        // Build the signed headers string
        $signedHeaders = array_keys($headers);
        sort($signedHeaders, SORT_STRING | SORT_FLAG_CASE);
        return implode(';', array_map('strtolower', $signedHeaders));
    }
}

$demo = new SignatureDemo();
$demo->main();